#!/usr/bin/env node
const http = require("http");
const fs = require("fs");
const os = require("os");
const path = require("path");
const { URL } = require("url");

function getIPv4List() {
  const nets = os.networkInterfaces();
  const results = new Set();
  for (const name of Object.keys(nets)) {
    for (const net of nets[name]) {
      if (net.family === "IPv4" && !net.internal) {
        results.add(net.address);
      }
    }
  }
  return [...results];
}

function findFreePort(startPort, cb) {
  const server = http.createServer();
  server.listen(startPort, () => {
    const port = server.address().port;
    server.close(() => cb(port));
  });
  server.on("error", () => findFreePort(startPort + 1, cb));
}

function countFiles(dir) {
  if (!fs.existsSync(dir)) return 0;
  return fs.readdirSync(dir).filter((f) => fs.statSync(path.join(dir, f)).isFile()).length;
}

// Send JSON
function sendJSON(res, code, obj) {
  if (res.headersSent) return;
  const data = Buffer.from(JSON.stringify(obj), "utf8");
  res.writeHead(code, {
    "Content-Type": "application/json; charset=utf-8",
    "Content-Length": data.length,
  });
  res.end(data);
}

// Guess MIME
function guessMime(filePath) {
  const ext = path.extname(filePath).toLowerCase();
  const map = {
    ".html": "text/html",
    ".js": "application/javascript",
    ".css": "text/css",
    ".png": "image/png",
    ".jpg": "image/jpeg",
    ".jpeg": "image/jpeg",
    ".gif": "image/gif",
    ".svg": "image/svg+xml",
    ".json": "application/json",
    ".txt": "text/plain",
    ".mp3": "audio/mpeg",
    ".wav": "audio/wav",
    ".aac": "audio/aac",
    ".flac": "audio/flac",
    ".ogg": "audio/ogg",
    ".mp4": "video/mp4",
    ".mov": "video/quicktime",
    ".avi": "video/x-msvideo",
    ".mkv": "video/x-matroska",
  };
  return map[ext] || "application/octet-stream";
}

function parseMultipart(req, boundary, uploadDir, cb) {
  let isDone = false;
  const done = (err) => {
    if (isDone) return;
    isDone = true;
    cb(err);
  };

  const boundaryBuf = Buffer.from("--" + boundary);
  let buffer = Buffer.alloc(0);
  let currentFile = null;
  let currentFilename = null;

  const contentLength = Number(req.headers["content-length"] || 0);
  let received = 0;

  req.on("data", (chunk) => {
    buffer = Buffer.concat([buffer, chunk]);
    received += chunk.length;

    while (true) {
      const idx = buffer.indexOf(boundaryBuf);
      if (idx === -1) {
        // Write everything except last part
        if (buffer.length > boundaryBuf.length) {
          if (currentFile) {
            currentFile.write(buffer.subarray(0, buffer.length - boundaryBuf.length));
          }
          buffer = buffer.subarray(-boundaryBuf.length);
        }
        break;
      }

      // Before boundary = file content
      const before = buffer.subarray(0, idx);
      if (currentFile) {
        const crlf = Buffer.from("\r\n");
        let endBuf = before;
        if (before.length >= crlf.length) {
          const tail = before.subarray(before.length - crlf.length);
          if (tail.length === crlf.length && tail.equals(crlf)) {
            endBuf = before.subarray(0, before.length - crlf.length);
          }
        }
        currentFile.write(endBuf);
        currentFile.end();
        currentFile = null;
        console.log("[upload]", currentFilename);
      }

      buffer = buffer.subarray(idx + boundaryBuf.length);

      // End of multipart
      if (buffer.subarray(0, 2).toString() === "--") {
        done(null);
        return;
      }

      // Skip CRLF
      if (buffer.subarray(0, 2).toString() === "\r\n") {
        buffer = buffer.subarray(2);
      }

      // Parse headers
      const headerEnd = buffer.indexOf("\r\n\r\n");
      if (headerEnd === -1) {
        break;
      }

      const headerText = buffer.subarray(0, headerEnd).toString("utf8");
      buffer = buffer.subarray(headerEnd + 4);

      let filename = null;
      headerText.split("\r\n").forEach((line) => {
        line = line.toLowerCase();
        if (line.startsWith("content-disposition:")) {
          const match = line.match(/filename="([^"]+)"/);
          if (match) filename = match[1];
        }
      });

      if (filename) {

        // --- 处理多重后缀 (.tar.gz) ---
        const parts = filename.split('.');
        let base, ext;

        if (parts.length > 2) {
          // abc.tar.gz → base: "abc", ext: ".tar.gz"
          base = parts.slice(0, -2).join('.');
          ext = '.' + parts.slice(-2).join('.');
        } else {
          // 普通情况 or 无后缀
          ext = path.extname(filename);
          base = filename.replace(ext, '');
        }

        let final = path.join(uploadDir, filename);
        let count = 1;

        // --- 碰到重名就生成 abc.1.tar.gz / abc.2.tar.gz ---
        while (fs.existsSync(final)) {
          let newName;

          if (base) {
            newName = `${base}.${count}${ext}`;   // 正常情况
          } else {
            newName = `${count}${ext}`;           // base 为空时 (极少见)
          }

          final = path.join(uploadDir, newName);
          count++;
        }

        currentFilename = final;
        currentFile = fs.createWriteStream(final);
      }

    }
  });

  req.on("end", () => done(null));
  req.on("error", (err) => done(err));
}


function logicDelete(absPath) {
  const parent = path.dirname(absPath);
  const parentName = path.basename(parent);
  const delDir = path.join(path.dirname(parent), parentName + "_delete");

  if (!fs.existsSync(delDir)) fs.mkdirSync(delDir, { recursive: true });

  let target = path.join(delDir, path.basename(absPath));
  const ext = path.extname(target);
  const base = path.basename(target, ext);

  let count = 1;
  while (fs.existsSync(target)) {
    target = path.join(delDir, `${base}(${count})${ext}`);
    count++;
  }

  fs.renameSync(absPath, target);
  return target;
}


function physicalDelete(absPath) {
  //有必要放回收站吗？直接删掉算了
  if (!fs.existsSync(absPath)) {
    return absPath; // 不存在就当做已删除
  }

  const stat = fs.statSync(absPath);

  if (stat.isDirectory()) {
    // 删除整个目录（递归）
    fs.rmSync(absPath, { recursive: true, force: true });
  } else {
    // 删除文件
    fs.unlinkSync(absPath);
  }

  return absPath;
}


function createServer(config) {
  const { baseDir, ext, maxcount, assetDir } = config;

  return http.createServer((req, res) => {
    const urlObj = new URL(req.url, `http://${req.headers.host}`);
    const pathname = decodeURIComponent(urlObj.pathname);

    // -------------- GET: /list ----------------
    if (pathname === "/list" && req.method === "GET") {
      const listDir = urlObj.searchParams.get("pathname") || "";
      const absDir = path.join(baseDir, listDir);

      if (!fs.existsSync(absDir) || !fs.statSync(absDir).isDirectory()) {
        return sendJSON(res, 500, { code: 500, error: "Not a directory" });
      }

      const result = [];

      fs.readdirSync(absDir).forEach((name) => {
        const fp = path.join(absDir, name);
        try {
          const st = fs.statSync(fp);
          result.push({
            file: name,
            filePath: path.join(listDir, name).replace(/\\/g, "/"),
            fileType: st.isDirectory() ? "/" : name.split(".").pop(),
            fileSize: st.size,
            fileModifiedTime: new Date(st.mtime).toISOString().slice(0, 19).replace("T", " "),
            error: "",
          });
        } catch (e) {
          result.push({
            file: name,
            filePath: path.join(listDir, name).replace(/\\/g, "/"),
            fileType: "",
            fileSize: 0,
            fileModifiedTime: "Not Permitted",
            error: e.toString(),
          });
        }
      });

      return sendJSON(res, 200, { code: 200, list: result, ext, maxcount });
    }

    // -------------- GET: static assets ----------------
    if (pathname.startsWith("/assets/")) {
      const filePath = path.join(assetDir, pathname.replace("/assets/", ""));
      if (!fs.existsSync(filePath)) {
        res.writeHead(404);
        return res.end("Asset Not Found");
      }

      res.writeHead(200, { "Content-Type": guessMime(filePath) });
      return fs.createReadStream(filePath).pipe(res);
    }

    const absPath = path.join(baseDir, pathname);

    // -------------- GET: file download with Range ----------------
    if (req.method === "GET" && fs.existsSync(absPath) && fs.statSync(absPath).isFile()) {
      const stat = fs.statSync(absPath);
      const fileSize = stat.size;

      const range = req.headers.range;
      if (range) {
        const match = /bytes=(\d+)-(\d*)/.exec(range);
        if (!match) {
          res.writeHead(416);
          return res.end();
        }
        const start = Number(match[1]);
        const end = match[2] ? Number(match[2]) : fileSize - 1;
        if (start >= fileSize) {
          res.writeHead(416, { "Content-Range": `bytes */${fileSize}` });
          return res.end();
        }

        res.writeHead(206, {
          "Content-Range": `bytes ${start}-${end}/${fileSize}`,
          "Content-Length": end - start + 1,
          "Content-Type": guessMime(absPath),
          "Accept-Ranges": "bytes",
        });

        return fs.createReadStream(absPath, { start, end }).pipe(res);
      }

      // normal download
      res.writeHead(200, {
        "Content-Type": guessMime(absPath),
        "Content-Length": fileSize,
        "Content-Disposition": `attachment; filename="${encodeURIComponent(path.basename(absPath))}"`,
        "Accept-Ranges": "bytes",
      });
      return fs.createReadStream(absPath).pipe(res);
    }

    // -------------- POST: upload ----------------
    if (req.method === "POST") {
      const uploadDir = absPath;
      if (maxcount && countFiles(uploadDir) >= maxcount) {
        return sendJSON(res, 400, { code: 400, error: "Exceeded max file count" });
      }

      if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });

      const ct = req.headers["content-type"] || "";
      if (!ct.includes("multipart/form-data")) {
        res.writeHead(400);
        return res.end("Expect multipart/form-data");
      }

      const boundaryMatch = /boundary="?([^";]+)"?/.exec(ct);
      if (!boundaryMatch) {
        res.writeHead(400);
        return res.end("Missing boundary");
      }

      const boundary = boundaryMatch[1];

      parseMultipart(req, boundary, uploadDir, (err) => {
        if (err) return sendJSON(res, 500, { code: 500, error: err.toString() });
        sendJSON(res, 200, { code: 200, msg: "ok" });
      });

      return;
    }

    // -------------- DELETE ----------------
    if (req.method === "DELETE") {
      const fp = urlObj.searchParams.get("filepath");
      if (!fp) return sendJSON(res, 400, { code: 400, error: "Missing filepath" });

      const safePath = fp.replace(/^[/\\]+/, '');  // remove leading / or \

      const absPath = path.resolve(baseDir, safePath);
      if (!absPath.startsWith(baseDir)) {
        return sendJSON(res, 403, { code: 403, error: "Permission denied" });
      }
      if (!fs.existsSync(absPath)) return sendJSON(res, 404, { code: 404, error: "Not found" });
      if (!fs.statSync(absPath).isFile()) return sendJSON(res, 400, { code: 400, error: "Not a file" });

      physicalDelete(absPath);
      console.log("[delete]", absPath);

      return sendJSON(res, 200, { code: 200, msg: "File deleted" });
    }

    // -------------- Default: return panel.html ----------------
    const panelFile = path.join(assetDir, "panel.html");
    if (fs.existsSync(panelFile)) {
      res.writeHead(200, { "Content-Type": "text/html" });
      return fs.createReadStream(panelFile).pipe(res);
    }

    res.writeHead(404);
    res.end("Not Found");
  });
}

// ---------------------- Startup ----------------------

function main() {
  const args = process.argv.slice(2);
  const getArg = (key, def) => {
    const idx = args.indexOf("--" + key);
    return idx !== -1 ? args[idx + 1] : def;
  };

  const port = Number(getArg("port", 8866));
  const baseDir = path.resolve(getArg("dir", process.cwd()));
  const ext = getArg("ext", null);
  const maxcount = getArg("maxcount", null);

  if (!fs.existsSync(baseDir)) fs.mkdirSync(baseDir, { recursive: true });

  const assetDir = path.join(__dirname, "assets");

  findFreePort(port, (freePort) => {
    const server = createServer({ baseDir, ext, maxcount, assetDir });

    server.listen(freePort, () => {
      console.log("\n=== http-file-server (Node.js) ===");
      console.log("Base dir:", baseDir);
      console.log();

      getIPv4List().forEach((ip) => console.log(`  http://${ip}:${freePort}/`));

      console.log("\nPress Ctrl+C to stop\n");
    });
  });
}

main();
